///PHILOTES Source Code.  (C)2012 PhiloLabs

#include "OgreStableHeaders.h"
#include "OgreDeflate.h"
#include "core/exception.h"

#include <zlib.h>

namespace Ogre
{
	// memory implementations
	void* OgreZalloc(void* opaque, unsigned int items, unsigned int size)
	{
		return PH_ALLOC_BYTES(items * size, Memory::ObjectHeap);
	}
	void OgreZfree(void* opaque, void* address)
	{
		PH_FREE(address, Memory::ObjectHeap);
	}
	#define OGRE_DEFLATE_TMP_SIZE 16384
    //---------------------------------------------------------------------
	DeflateStream::DeflateStream(const DataStreamPtr& compressedStream)
	: DataStream(compressedStream->getAccessMode())
	, mCompressedStream(compressedStream)
	, mZStream(0)
	, mCurrentPos(0)
	, mTmp(0)
	, mIsCompressedValid(true)
	{
		init();
	}
    //---------------------------------------------------------------------
	DeflateStream::DeflateStream(const String& name, const DataStreamPtr& compressedStream)		
	: DataStream(name, compressedStream->getAccessMode())
	, mCompressedStream(compressedStream)
	, mZStream(0)
	, mCurrentPos(0)
	, mTmp(0)
	, mIsCompressedValid(true)
	{
		init();
	}
    //---------------------------------------------------------------------
	void DeflateStream::init()
	{
		mZStream = PH_ALLOC_T(z_stream, 1, Memory::ObjectHeap);
		mZStream->zalloc = OgreZalloc;
		mZStream->zfree = OgreZfree;
		
		if (getAccessMode() == READ)
		{
			mTmp = (unsigned char*)PH_ALLOC_BYTES(OGRE_DEFLATE_TMP_SIZE, Memory::ObjectHeap);
			size_t restorePoint = mCompressedStream->tell();
			// read early chunk
			mZStream->next_in = mTmp;
			mZStream->avail_in = mCompressedStream->read(mTmp, OGRE_DEFLATE_TMP_SIZE);
			
			if (inflateInit(mZStream) != Z_OK)
			{
				mIsCompressedValid = false;
			}
			else
				mIsCompressedValid = true;
			
			if (mIsCompressedValid)
			{
				// in fact, inflateInit on some implementations doesn't try to read
				// anything. We need to at least read something to test
				Bytef testOut[4];
				size_t savedIn = mZStream->avail_in;
				mZStream->avail_out = 4;
				mZStream->next_out = testOut;
				if (inflate(mZStream, Z_SYNC_FLUSH) != Z_OK)
					mIsCompressedValid = false;
				// restore for reading
				mZStream->avail_in = savedIn;
				mZStream->next_in = mTmp;

				inflateReset(mZStream);
			}

			if (!mIsCompressedValid)
			{
				// Not compressed data!
				// Fail gracefully, fall back on reading the underlying stream direct
				destroy();
				mCompressedStream->seek(restorePoint);
			}				
		}
		else 
		{
			// Write to temp file
			char tmpname[L_tmpnam];
			tmpnam(tmpname);
			mTempFileName = tmpname;
			std::fstream *f = PH_NEW_T(std::fstream, Memory::ObjectHeap)();
			f->open(tmpname, std::ios::binary | std::ios::out);
			mTmpWriteStream = DataStreamPtr(PHILO_NEW FileStreamDataStream(f));
			
		}

	}
    //---------------------------------------------------------------------
	void DeflateStream::destroy()
	{
		if (getAccessMode() == READ)
			inflateEnd(mZStream);

		PH_FREE(mZStream, Memory::ObjectHeap);
		mZStream = 0;
		PH_FREE(mTmp, Memory::ObjectHeap);
		mTmp = 0;
	}
	//---------------------------------------------------------------------
	DeflateStream::~DeflateStream()
	{
		close();
		destroy();
	}
    //---------------------------------------------------------------------
	size_t DeflateStream::read(void* buf, size_t count)
	{
		if (!mIsCompressedValid)
		{
			return mCompressedStream->read(buf, count);
		}
		
		if (getAccessMode() & WRITE)
		{
			return mTmpWriteStream->read(buf, count);
		}
		else 
		{

			size_t restorePoint = mCompressedStream->tell();
			// read from cache first
			size_t cachereads = mReadCache.read(buf, count);
			
			size_t newReadUncompressed = 0;

			if (cachereads < count)
			{
				mZStream->avail_out = count - cachereads;
				mZStream->next_out = (Bytef*)buf + cachereads;
				
				while (mZStream->avail_out)
				{
					// Pull next chunk of compressed data from the underlying stream
					if (!mZStream->avail_in && !mCompressedStream->eof())
					{
						mZStream->avail_in = mCompressedStream->read(mTmp, OGRE_DEFLATE_TMP_SIZE);
						mZStream->next_in = mTmp;
					}
					
					if (mZStream->avail_in)
					{
						int availpre = mZStream->avail_out;
						int status = inflate(mZStream, Z_SYNC_FLUSH);
						size_t readUncompressed = availpre - mZStream->avail_out;
						newReadUncompressed += readUncompressed;
						if (status != Z_OK)
						{
							// End of data, or error
							if (status != Z_STREAM_END)
							{
								mCompressedStream->seek(restorePoint);
								PHILO_EXCEPT(Exception::ERR_INVALID_STATE, 
											"Error in compressed stream",
											"DeflateStrea::read");
							}
							else 
							{
								// back up the stream so that it can be used from the end onwards													
								long unusedCompressed = mZStream->avail_in;
								mCompressedStream->skip(-unusedCompressed);
							}

							break;
						}
					}
				}
			}
			
			// Cache the last bytes read
			mReadCache.cacheData((char*)buf + cachereads, newReadUncompressed);
			
			mCurrentPos += newReadUncompressed + cachereads;
			
			return newReadUncompressed + cachereads;
		}
	}
    //---------------------------------------------------------------------
	size_t DeflateStream::write(const void* buf, size_t count)
	{
		if ((getAccessMode() & WRITE) == 0)
			PHILO_EXCEPT(Exception::ERR_INVALIDPARAMS,
						"Not a writable stream", "DeflateStream::write");
		
		return mTmpWriteStream->write(buf, count);
	}
    //---------------------------------------------------------------------
	void DeflateStream::compressFinal()
	{
		// Close temp stream
		mTmpWriteStream->close();
		
		// Copy & compress
		// We do this rather than compress directly because some code seeks
		// around while writing (e.g. to update size blocks) which is not
		// possible when compressing on the fly
		
		int ret, flush;
		char in[OGRE_DEFLATE_TMP_SIZE];
		char out[OGRE_DEFLATE_TMP_SIZE];
		
		if (deflateInit(mZStream, Z_DEFAULT_COMPRESSION) != Z_OK)
		{
			destroy();
			PHILO_EXCEPT(Exception::ERR_INVALID_STATE, 
						"Error initialising deflate compressed stream!",
						"DeflateStream::init");
		}
		
		std::ifstream inFile;
		inFile.open(mTempFileName.c_str(), std::ios::in | std::ios::binary);
		
		do 
		{
			inFile.read(in, OGRE_DEFLATE_TMP_SIZE);
			mZStream->avail_in = inFile.gcount();
			if (inFile.bad()) 
			{
				deflateEnd(mZStream);
				PHILO_EXCEPT(Exception::ERR_INVALID_STATE, 
							"Error reading temp uncompressed stream!",
							"DeflateStream::init");
			}
			flush = inFile.eof() ? Z_FINISH : Z_NO_FLUSH;
			mZStream->next_in = (Bytef*)in;
			
			/* run deflate() on input until output buffer not full, finish
			 compression if all of source has been read in */
			do 
			{
				mZStream->avail_out = OGRE_DEFLATE_TMP_SIZE;
				mZStream->next_out = (Bytef*)out;
				ret = deflate(mZStream, flush);    /* no bad return value */
				assert(ret != Z_STREAM_ERROR);  /* state not clobbered */
				size_t compressed = OGRE_DEFLATE_TMP_SIZE - mZStream->avail_out;
				mCompressedStream->write(out, compressed);
			} while (mZStream->avail_out == 0);
			assert(mZStream->avail_in == 0);     /* all input will be used */
			
			/* done when last data in file processed */
		} while (flush != Z_FINISH);
		assert(ret == Z_STREAM_END);        /* stream will be complete */
		
		deflateEnd(mZStream);
				
		remove(mTempFileName.c_str());
						
	}
    //---------------------------------------------------------------------
	void DeflateStream::skip(long count)
	{
		if (!mIsCompressedValid)
		{
			mCompressedStream->skip(count);
			return;
		}
		
		if (getAccessMode() & WRITE)
		{
			mTmpWriteStream->skip(count);
		}
		else 
		{
			if (count > 0)
			{
				if (!mReadCache.ff(count))
				{
					PHILO_EXCEPT(Exception::ERR_INVALID_STATE, 
								"You can only skip within the cache range in a deflate stream.",
								"DeflateStream::skip");
				}
			}
			else if (count < 0)
			{
				if (!mReadCache.rewind((size_t)(-count)))
				{
					PHILO_EXCEPT(Exception::ERR_INVALID_STATE, 
								"You can only skip within the cache range in a deflate stream.",
								"DeflateStream::skip");
				}
			}
		}		
		mCurrentPos = static_cast<size_t>(static_cast<long>(mCurrentPos) + count);
		
		
	}
    //---------------------------------------------------------------------
	void DeflateStream::seek( size_t pos )
	{
		if (!mIsCompressedValid)
		{
			mCompressedStream->seek(pos);
			return;
		}
		if (getAccessMode() & WRITE)
		{
			mTmpWriteStream->seek(pos);
		}
		else
		{
			if (pos == 0)
			{
				mCurrentPos = 0;
				mZStream->next_in = mTmp;
				mCompressedStream->seek(0);
				mZStream->avail_in = mCompressedStream->read(mTmp, OGRE_DEFLATE_TMP_SIZE);			
				inflateReset(mZStream);
			}
			else 
			{
				skip(pos - tell());
			}
		}		
	}
    //---------------------------------------------------------------------
	size_t DeflateStream::tell(void) const
	{
		if (!mIsCompressedValid)
		{
			return mCompressedStream->tell();
		}
		else if(getAccessMode() & WRITE) 
		{
			return mTmpWriteStream->tell();
		}
		else
		{
			return mCurrentPos;
		}

	}
    //---------------------------------------------------------------------
	bool DeflateStream::eof(void) const
	{
		if (getAccessMode() & WRITE)
			return mTmpWriteStream->eof();
		else 
		{
			if (!mIsCompressedValid)
				return mCompressedStream->eof();
			else
				return mCompressedStream->eof() && mZStream->avail_in == 0;
		}
	}
    //---------------------------------------------------------------------
	void DeflateStream::close(void)
	{
		if (getAccessMode() & WRITE)
		{
			compressFinal();
		}
		
		// don't close underlying compressed stream in case used for something else
	}
    //---------------------------------------------------------------------
	
	
}


